/*
 * Copyright 2008 The Microlog project @sourceforge.net
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sf.microlog.instrument;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Vector;

import javassist.CannotCompileException;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;
import net.sf.jour.filter.Pointcut;
import net.sf.jour.filter.PointcutListFilter;
import net.sf.jour.instrumentor.Instrumentor;
import net.sf.jour.instrumentor.InstrumentorResults;
import net.sf.jour.instrumentor.InstrumentorResultsImpl;
import net.sf.microlog.core.Logger;

/**
 * This calls implements the instrumentation for Microlog.
 * 
 * @author Karsten Ohme
 * 
 */
public class MicrologInstrumentor implements Instrumentor {

	/**
	 * Logger instance.
	 */
	protected static final Logger log = Logger
			.getLogger(MicrologInstrumentor.class);

	/**
	 * The point cuts of the aspect.
	 */
	protected PointcutListFilter pointcuts;

	/**
	 * Remove all log information?
	 */
	private boolean remove;

	public void remove(String remove) {
		this.remove = Boolean.valueOf(remove).booleanValue();
	}

	/**
	 * Holds the status within a class instrumentation.
	 * 
	 * @author Karsten Ohme
	 * 
	 */
	private class InstrumentatorStatus {

		/**
		 * The class was modified.
		 */
		private boolean modified;

		/**
		 * Map holding the names of instrumented methods.
		 */
		private final Set instrumentedMethods;

		/**
		 * Constructor.
		 */
		public InstrumentatorStatus() {
			instrumentedMethods = new HashSet();
		}

		/**
		 * Returns if the method was modified.
		 * 
		 * @return <code>true</code> if modified
		 */
		public boolean isModified() {
			return modified;
		}

		/**
		 * Sets the status to modified or unmodified.
		 * 
		 * @param modified
		 *            <code>true</code> if modified.
		 */
		public void setModified(boolean modified) {
			this.modified = modified;
		}

		/**
		 * Returns the number of instrumented methods.
		 * 
		 * @return the number of instrumented methods
		 */
		public int getMethodCount() {
			return instrumentedMethods.size();
		}

		/**
		 * Marks the method as instrumented.
		 */
		public void setInstrumented(String methodName) {
			instrumentedMethods.add(methodName);
		}
	}

	/**
	 * 
	 * {@inheritDoc}
	 */
	public InstrumentorResults instrument(CtClass clazz) {

		if (clazz.isInterface()
				|| clazz.getName().startsWith("net.sf.microlog")) {
			return InstrumentorResultsImpl.NOT_MODIFIED;
		}

		final InstrumentatorStatus status = new InstrumentatorStatus();

		List createdClasses = new Vector();

		for (Iterator i = pointcuts.iterator(); i.hasNext();) {
			Pointcut pointcut = (Pointcut) i.next();

			if (pointcut.acceptClass(clazz)) {

				// the class which is generated in case of instrumentation
				createdClasses.add(clazz);
				Iterator methods = Arrays.asList(clazz.getDeclaredMethods())
						.iterator();
				while (methods.hasNext()) {
					CtMethod method = (CtMethod) methods.next();
					if (pointcut.acceptMethod(method)
							&& pointcuts.match(method)) {

						log.debug("found method:" + clazz.getName() + "."
								+ method.getName() + "("
								+ method.getSignature() + ")");

						// remove logging information
						if (remove) {
						} else {
							final String methodName = method.getName();
							final String methodsignature = method
									.getSignature();
							final String className = method.getDeclaringClass()
									.getName();
							final String classSimpleName = method
									.getDeclaringClass().getSimpleName();

							try {
								method.instrument(new ExprEditor() {
									public void edit(MethodCall m)
											throws CannotCompileException {
										if (m.getClassName().equals(
												net.sf.microlog.core.Logger.class
														.getName())) {
											// mark as modified
											status.setModified(true);
											// increment counter
											status.setInstrumented(m
													.getMethodName());
											// get number of parameters - found
											// no
											// better way
											String signature = m.getSignature();
											int numParams = signature
													.split(";").length - 1;
											// the replacement method string
											StringBuffer replacement = new StringBuffer();
											// the reference to the Logger
											// class.
											replacement.append("$0.");
											replacement.append(m
													.getMethodName());
											replacement.append('(');
											// build additional log information
											String info = additionalLogInfo(
													className, methodName,
													classSimpleName, m
															.getLineNumber());
											// handle implicit logging methods
											if (m.getMethodName().equals(
													"debug")
													|| m.getMethodName()
															.equals("info")
													|| m.getMethodName()
															.equals("fatal")
													|| m.getMethodName()
															.equals("error")
													|| m.getMethodName()
															.equals("warn")
													|| m.getMethodName()
															.equals("trace")) {
												replacement.append(info);
												if (numParams == 1) {
													replacement
															.append("+ $1);");
												} else if (numParams == 2) {
													replacement
															.append("+ $1, $2);");
												} else {
													throw new RuntimeException(
															"Unsupported number of parameters for log method.");
												}
												log
														.debug("inserting replacement: "
																+ replacement);
												m.replace(replacement
														.toString());
											}
											// handle explicit logging methods
											else if (m.getMethodName().equals(
													"log")) {
												if (numParams == 2) {
													replacement.append("$1, ");
													replacement.append(info);
													replacement
															.append("+ $2);");
												} else if (numParams == 3) {
													replacement.append("$1, ");
													replacement.append(info);
													replacement
															.append("+ $2, $3);");
												} else {
													throw new RuntimeException(
															"Unsupported number of parameters for log method.");
												}
												log
														.debug("inserting replacement: "
																+ replacement);
												m.replace(replacement
														.toString());
											}
										}
									}
								});
							} catch (CannotCompileException e) {
								throw new RuntimeException("Cannot compile.", e);
							}

						}
					}
				}
			}
		}

		if (status.isModified()) {
			log.debug("instrumented:" + clazz.getName()
					+ " instrumented methods: " + status.getMethodCount()
					+ " created classes: " + createdClasses);
		} else {
			log.debug("not instrumented:" + clazz.getName());
		}

		log.debug("end instrumenting:" + clazz.getName());
		if (status.isModified()) {
			return new InstrumentorResultsImpl(0, status.getMethodCount(),
					createdClasses);
		} else {
			return InstrumentorResultsImpl.NOT_MODIFIED;
		}
	}

	/**
	 * Builds the additional log information.
	 * 
	 * @param className
	 *            The class name where the logger is called.
	 * @param methodName
	 *            The method name where the logger is called.
	 * @param classSimpleName
	 *            The simple class name.
	 * @param lineNumber
	 *            The line number
	 * @return The additional log information.
	 */
	private String additionalLogInfo(String className, String methodName,
			String classSimpleName, int lineNumber) {
		StringBuffer sb = new StringBuffer();
		sb.append("\"");
		sb.append(className);
		sb.append('.');
		sb.append(methodName);
		sb.append("(");
		sb.append(classSimpleName);
		sb.append(".java:");
		sb.append(lineNumber);
		sb.append("): \"");
		return sb.toString();
	}

	/**
	 * 
	 * {@inheritDoc}
	 */
	public void setPointcuts(PointcutListFilter pointcuts) {
		this.pointcuts = pointcuts;
	}

}
